<?php

if (!defined('YII_PATH'))
    exit('No direct script access allowed');

/**
 * CmsSettings
 * 
 * @package OneTwist CMS  
 * @author twisted1919 (cristian.serban@onetwist.com)
 * @copyright OneTwist CMS (www.onetwist.com)
 * @version 1.2
 * @since 1.0
 * @access public
 *
 * 1.1e - Special thanks to Gustavo (http://www.yiiframework.com/forum/index.php?/user/6112-gustavo/)
 */
class CmsSettings extends CApplicationComponent {

    protected $saveItemsToDatabase = array();
    protected $deleteItemsFromDatabase = array();
    protected $deleteCategoriesFromDatabase = array();
    protected $cacheNeedsFlush = array();
    protected $cacheRegistry = array();
    protected $initCacheRegistry = array();
    protected $deleteFromCacheRegistry = array();
    protected $items = array();
    protected $loaded = array();
    protected $_cacheComponentId = 'cache';
    protected $_cacheId = 'global_website_settings';
    protected $_cacheTime = 0;
    protected $_dbComponentId = 'db';
    protected $_tableName = '{{settings}}';
    protected $_createTable = false;
    protected $_dbEngine = 'InnoDB';

    public function init() {
        parent::init();
        Yii::app()->attachEventHandler('onEndRequest', array($this, 'whenRequestEnds'));

        if ($this->getCreateTable())
            $this->createTable();
    }

    /**
     * CmsSettings::set()
     * 
     * @param string $category name of the category 
     * @param mixed $key 
     * can be either a single item (string) or an array of item=>value pairs 
     * @param mixed $value value to set for the key, leave this empty if $key is an array
     * @param bool $toDatabase whether to save the items to the database
     * @return CmsSettings
     */
    public function set($category = 'system', $key = '', $value = '', $toDatabase = true) {
        if (is_array($key)) {
            foreach ($key AS $k => $v)
                $this->set($category, $k, $v, $toDatabase);
        } else {
            if ($toDatabase) {
                if (isset($this->saveItemsToDatabase[$category]) && is_array($this->saveItemsToDatabase[$category]))
                    $this->saveItemsToDatabase[$category] = CMap::mergeArray($this->saveItemsToDatabase[$category], array($key => $value));
                else
                    $this->saveItemsToDatabase[$category] = array($key => $value);
            }
            if (isset($this->items[$category]) && is_array($this->items[$category]))
                $this->items[$category] = CMap::mergeArray($this->items[$category], array($key => $value));
            else
                $this->items[$category] = array($key => $value);
        }
        return $this;
    }

    /**
     * CmsSettings::get()
     * 
     * @param string $category name of the category
     * @param mixed $key 
     * can be either :
     * empty, returning all items of the selected category
     * a string, meaning a single key will be returned
     * an array, returning an array of key=>value pairs  
     * @param string $default the default value to be returned
     * @return mixed
     */
    public function get($category = 'system', $key = '', $default = null) {
        if (!isset($this->loaded[$category]))
            $this->load($category);

        if (empty($key) && empty($default) && !empty($category))
            return isset($this->items[$category]) ? $this->items[$category] : null;

        if (!empty($key) && is_array($key)) {
            $toReturn = array();
            foreach ($key AS $k => $v) {
                if (is_numeric($k))
                    $toReturn[$v] = $this->get($category, $v);
                else
                    $toReturn[$k] = $this->get($category, $k, $v);
            }
            return $toReturn;
        }

        if (isset($this->items[$category][$key]))
            return $this->items[$category][$key];
        return $default;
    }

    /**
     * delete an item or all items from a category
     * 
     * @param string $category the name of the category 
     * @param mixed $key 
     * can be either:
     * empty, meaning it will delete all items of the selected category 
     * a single key
     * an array of keys
     * @return CmsSettings
     */
    public function delete($category, $key = '') {
        if (empty($category))
            return $this;

        if (!empty($category) && empty($key)) {
            $this->deleteCategoriesFromDatabase[] = $category;
            $this->deleteCache($category);
            if (isset($this->items[$category]))
                unset($this->items[$category]);
            return;
        }
        if (is_array($key)) {
            foreach ($key AS $k)
                $this->delete($category, $k);
        } else {
            if (isset($this->items[$category][$key])) {
                unset($this->items[$category][$key]);
                if (empty($this->deleteItemsFromDatabase[$category]))
                    $this->deleteItemsFromDatabase[$category] = array();
                $this->deleteItemsFromDatabase[$category][] = $key;
            }
        }
        return $this;
    }

    /**
     * load the cache registry
     * @return $cacheRegistry array containing all the cached categories
     */
    protected function loadCacheRegistry() {
        if (!empty($this->cacheRegistry))
            return $this->cacheRegistry;

        $cacheRegistry = $this->getCacheComponent()->get('__cache_registry_' . $this->getCacheId());

        if (empty($cacheRegistry) || !is_array($cacheRegistry))
            $cacheRegistry = array();

        $this->cacheRegistry = $cacheRegistry;
        $this->initCacheRegistry = $cacheRegistry;

        return $this->cacheRegistry;
    }

    /**
     * add to cache registry
     * 
     * @param $category - the category to be added to cache.
     * @return CmsSettings
     */
    protected function addToCacheRegistry($category) {
        $cacheRegistry = $this->loadCacheRegistry();
        if (!in_array($category, $cacheRegistry)) {
            $this->cacheRegistry[] = $category;
        }
        return $this;
    }

    /**
     * delete one/more/all categories from cache
     * 
     * @param mixed $category the name of the category 
     * if $category is empty will delete all cached categories.
     * if $category is an array, will delete all provided categories
     * if $category is a string, will delete only that particular category
     * @return CmsSettings
     */
    public function deleteCache($category = '') {
        $cacheRegistry = $this->loadCacheRegistry();

        if (empty($category)) {
            $this->deleteFromCacheRegistry = CMap::mergeArray($this->deleteFromCacheRegistry, $cacheRegistry);
            $cacheRegistry = array();
        } elseif (is_string($category) && in_array($category, $cacheRegistry)) {
            unset($cacheRegistry[array_search($category, $cacheRegistry)]);
            $this->deleteFromCacheRegistry[] = $category;
        } elseif (is_array($category)) {
            foreach ($category AS $catName) {
                if (in_array($catName, $cacheRegistry)) {
                    unset($cacheRegistry[array_search($catName, $cacheRegistry)]);
                    $this->deleteFromCacheRegistry[] = $catName;
                }
            }
        }
        $this->cacheRegistry = $cacheRegistry;
        return $this;
    }

    /**
     * load from database the items of the specified category
     * 
     * @param string $category
     * @return array the items of the category
     */
    public function load($category) {
        $items = $this->getCacheComponent()->get($category . '_' . $this->getCacheId());
        $this->loaded[$category] = true;
        $this->addToCacheRegistry($category);

        if (!$items) {
            $connection = $this->getDbComponent();
            $command = $connection->createCommand('SELECT `key`, `value` FROM ' . $this->getTableName() . ' WHERE category=:cat');
            $command->bindParam(':cat', $category);
            $result = $command->queryAll();

            if (empty($result))
                return;

            $items = array();
            foreach ($result AS $row)
                $items[$row['key']] = @unserialize($row['value']);
            $this->getCacheComponent()->set($category . '_' . $this->getCacheId(), $items, $this->getCacheTime());
        }

        if (isset($this->items[$category]))
            $items = CMap::mergeArray($items, $this->items[$category]);

        $this->set($category, $items, null, false);
        return $items;
    }

    public function toArray() {
        return $this->items;
    }

    /**
     * @param int $int the time to cache the keys, defaults to 0
     */
    public function setCacheTime($int) {
        $this->_cacheTime = (int) $int > 0 ? $int : 0;
    }

    /**
     * @return int the time to cache the keys, defaults to 0
     */
    public function getCacheTime() {
        return $this->_cacheTime;
    }

    /**
     * @param string $str the cache key to prepend to all categories, defaults to 'global_website_settings'
     */
    public function setCacheId($str = '') {
        $this->_cacheId = !empty($str) ? $str : $this->_cacheId;
    }

    /**
     * @return string the cache key to prepend to all categories, defaults to 'global_website_settings'
     */
    public function getCacheId() {
        return $this->_cacheId;
    }

    /**
     * @param string $name the name of the cache component to use, defaults to 'cache'
     */
    public function setCacheComponentId($name) {
        $this->_cacheComponentId = $name;
    }

    /**
     * @return string the name of the cache component to use, defaults to 'cache'
     */
    public function getCacheComponentId() {
        return $this->_cacheComponentId;
    }

    /**
     * @param $name string the name of the settings database table, defaults to '{{settings}}'
     */
    public function setTableName($name) {
        if ($this->getCreateTable() && (strpos($name, '{{') != 0 || strpos($name, '}}') != (strlen($name) - 2)))
            throw new CException('The table name must be like "{{' . $name . '}}" not just "' . $name . '"');
        $this->_tableName = $name;
    }

    /**
     * @return string the name of the settings database table, defaults to '{{settings}}'
     */
    public function getTableName() {
        return $this->_tableName;
    }

    /**
     * @param string $name the name of the db component to use, defaults to 'db'
     */
    public function setDbComponentId($name) {
        $this->_dbComponentId = $name;
    }

    /**
     * @return string the name of the db component to use, defaults to 'db'
     */
    public function getDbComponentId() {
        return $this->_dbComponentId;
    }

    /**
     * wheter to create the settings table if the table does not exist
     * set this to false in production mode as it will slow down the application
     * defaults to false
     * @param boolean $bool
     */
    public function setCreateTable($bool) {
        $this->_createTable = (bool) $bool;
    }

    /**
     * wheter to create the settings table if the table does not exist
     * set this to false in production mode as it will slow down the application
     * defaults to false
     * @return boolean
     */
    public function getCreateTable() {
        return $this->_createTable;
    }

    /**
     * @param string $name the engine to use when creating a new table, defaults to 'InnoDb'
     */
    public function setDbEngine($name) {
        $this->_dbEngine = $name;
    }

    /**
     * @return string the dbEngine to use when creating a new table, defaults to 'InnoDb'
     */
    public function getDbEngine() {
        return $this->_dbEngine;
    }

    /**
     * @return CCache the cache component
     */
    protected function getCacheComponent() {
        return Yii::app()->getComponent($this->getCacheComponentId());
    }

    /**
     * @return CDbConnection the db connection component
     */
    protected function getDbComponent() {
        return Yii::app()->getComponent($this->getDbComponentId());
    }

    protected function addDbItem($category = 'system', $key, $value) {
        $connection = $this->getDbComponent();
        $command = $connection->createCommand('SELECT id FROM ' . $this->getTableName() . ' WHERE `category`=:cat AND `key`=:key LIMIT 1');
        $command->bindParam(':cat', $category);
        $command->bindParam(':key', $key);
        $result = $command->queryRow();
        $value = @serialize($value);

        if (!empty($result))
            $command = $connection->createCommand('UPDATE ' . $this->getTableName() . ' SET `value`=:value WHERE `category`=:cat AND `key`=:key');
        else
            $command = $connection->createCommand('INSERT INTO ' . $this->getTableName() . ' (`category`,`key`,`value`) VALUES(:cat,:key,:value)');

        $command->bindParam(':cat', $category);
        $command->bindParam(':key', $key);
        $command->bindParam(':value', $value);
        $command->execute();
    }

    protected function whenRequestEnds() {
        $this->cacheNeedsFlush = array();

        if (count($this->deleteCategoriesFromDatabase) > 0) {
            foreach ($this->deleteCategoriesFromDatabase AS $catName) {
                $connection = $this->getDbComponent();
                $command = $connection->createCommand('DELETE FROM ' . $this->getTableName() . ' WHERE `category`=:cat');
                $command->bindParam(':cat', $catName);
                $command->execute();
                $this->cacheNeedsFlush[] = $catName;

                if (isset($this->deleteItemsFromDatabase[$catName]))
                    unset($this->deleteItemsFromDatabase[$catName]);
                if (isset($this->saveItemsToDatabase[$catName]))
                    unset($this->saveItemsToDatabase[$catName]);
            }
        }

        if (count($this->deleteItemsFromDatabase) > 0) {
            foreach ($this->deleteItemsFromDatabase AS $catName => $keys) {
                $params = array();
                $i = 0;
                foreach ($keys AS $v) {
                    if (isset($this->saveItemsToDatabase[$catName][$v]))
                        unset($this->saveItemsToDatabase[$catName][$v]);
                    $params[':p' . $i] = $v;
                    ++$i;
                }
                $names = implode(',', array_keys($params));

                $connection = $this->getDbComponent();
                $query = 'DELETE FROM ' . $this->getTableName() . ' WHERE `category`=:cat AND `key` IN(' . $names . ')';
                $command = $connection->createCommand($query);
                $command->bindParam(':cat', $catName);

                foreach ($params AS $key => $value)
                    $command->bindParam($key, $value);

                $command->execute();
                $this->cacheNeedsFlush[] = $catName;
            }
        }

        if (count($this->saveItemsToDatabase) > 0) {
            foreach ($this->saveItemsToDatabase AS $catName => $keyValues) {
                foreach ($keyValues AS $k => $v)
                    $this->addDbItem($catName, $k, $v);
                $this->cacheNeedsFlush[] = $catName;
            }
        }

        if (count($this->cacheRegistry) == 0 && count($this->initCacheRegistry) > 0)
            $this->getCacheComponent()->delete('__cache_registry_' . $this->getCacheId());
        elseif (count(array_diff($this->initCacheRegistry, $this->cacheRegistry)) > 0 || count(array_diff($this->cacheRegistry, $this->initCacheRegistry)) > 0)
            $this->getCacheComponent()->set('__cache_registry_' . $this->getCacheId(), $this->cacheRegistry, $this->getCacheTime());

        if (count($this->deleteFromCacheRegistry) > 0)
            $this->cacheNeedsFlush = array_unique(CMap::mergeArray($this->cacheNeedsFlush, $this->deleteFromCacheRegistry));

        if (count($this->cacheNeedsFlush) > 0) {
            foreach ($this->cacheNeedsFlush AS $catName)
                $this->getCacheComponent()->delete($catName . '_' . $this->getCacheId());
        }
    }

    /**
     * create the settings table
     */
    protected function createTable() {
        $connection = $this->getDbComponent();
        $tableName = $connection->tablePrefix . str_replace(array('{{', '}}'), '', $this->getTableName());
        $sql = 'CREATE TABLE IF NOT EXISTS `' . $tableName . '` (
		  `id` int(11) NOT NULL auto_increment,
		  `category` varchar(64) NOT NULL default \'system\',
		  `key` varchar(255) NOT NULL,
		  `value` text NOT NULL,
		  PRIMARY KEY  (`id`),
		  KEY `category_key` (`category`,`key`)
		) ' . ($this->getDbEngine() ? 'ENGINE=' . $this->getDbEngine() : '') . '  DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ; ';
        $command = $connection->createCommand($sql);
        $command->execute();
    }

}